#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<ctype.h>
#include<errno.h>

#define LEN 1024//命令行可输入的最大字符串长度
#define NUM 64//命令行字符串分割后，命令行参数的最大个数

#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3

#define trimSpace(start) do{\
    while(isspace(*start)) ++start;\
}while(0)//不用加分号，直接文本替换

char Command[LEN];//存储我们输入的命令（本质是字符串）"ls -a -l"
char* myargv[NUM];//指针数组，存的是分割后的每个命令行参数的首元素地址 "ls""-a""-l"

int  lastCode = 0;
int  lastSig = 0;

int redirType = NONE_REDIR;
char *redirFile = NULL;

//"ls -a -l -i" > myfile.txt"-> "ls -a -l -i" "myfile.txt"//这个函数要做的就是把命令分割成两个字符串
void commandCheck(char* commands)
{
    assert(commands);
    char* start = commands;
    char* end=commands + strlen(commands);

    while(start < end)
    {
        if(*start == '>')
        {
            *start ='\0';
            start++;
            //如果是两个>,则是追加重定向
            if(*start == '>')
            {
                //"ls -a >> file.log"
                redirType = APPEND_REDIR;
                start++;
            }
            else
            {
                //"ls -a >    file.log"
                redirType = OUTPUT_REDIR;
            }
            //跳过空格，这里用宏函数实现，所以宏函数不用加分号
            trimSpace(start);
            //拿到重定向到的对象文件名首元素地址
            redirFile = start;
            break;
        }
        else if(*start == '<')
        {
            //cat >    file.txt
            *start='\0';
            start++;
            trimSpace(start);
            redirType = INPUT_REDIR;
            redirFile=start;
            break;
        }
        else
        {
            start++;
        }
    }
}

int main()
{

    extern char** environ;
    //命令行解释器需要一直输出
    while(1)
    {
        redirType = NONE_REDIR;
        redirFile = NULL;
        errno = 0;
        //输出命令行解释器
        printf("[用户名@主机名 当前路径]# ");
        fflush(stdout);

        //获取用户输入，我们最后都会输入'\n'【注意】
        //char *fgets(char *s, int size, FILE *stream);
        char* s=fgets(Command,sizeof(Command)-1,stdin);
        assert(s!=NULL);//输入不能为空
        (void)s;
        //清除最后一个'\n',避免输出再换行
        //ls -a -l\n'\0'   abc\n\0
        Command[strlen(Command)-1]=0;

        //对重定向的处理
        commandCheck(Command);
        //printf("test: %s\n",Command);
        
        //字符串切割
        myargv[0]=strtok(Command," ");
        int i=1;
        if(myargv[0]!=NULL&&strcmp(myargv[0],"ls")==0)//判断myargv[0]是否切割正常，穷举指令，为指令添加选项
        {
            myargv[i++]=(char*)"--color=auto";//穷举命令，给命令带上颜色的选项
        }
        while((myargv[i++] = strtok(NULL, " ")));

        //如果是cd命令，我们不需要创建子进程，让shell自己执行对应的命令
        //本质就是执行系统接口,像这种不需要我们的子进程执行，而是让shell执行的命令
        //叫做内建命令,内建命令是父进程base执行的，相当于base的函数
        if(myargv[0]!=NULL && strcmp(myargv[0],"cd")==0)
        {
            if(myargv[1]!=NULL) chdir(myargv[1]);//改变父进程的当前路径
            continue;//跳出本轮循环，后序子进程可以不用创建，因为按照意愿更改了父进程当前路径
                //所以下次输入命令pwd，我们就是在我们更改的路径下
        }
        else if(strcmp(myargv[0],"export")==0)//
        {
            if(myargv[1]!=NULL) putenv(myargv[1]);
            continue;
        }
        if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
        {
            const char* target_env=NULL;
            if(strcmp(myargv[1], "$?") == 0)
            {
                printf("退出码%d 终止信号 %d\n", lastCode, lastSig);
                continue;
            }
            else
            {
                target_env=getenv(myargv[1]+1);
            }
            if(target_env!=NULL) printf("%s=%s\n",myargv[1],target_env);
            continue;
        }
        //测试是否成功：条件编译
#ifdef DEBUG
        for(i=0;myargv[i];i++)
        {
            printf("myargv[%d]: %s\n",i,myargv[i]);
        }
#endif
        //内建命令echo
        
        pid_t id=fork();//让子进程执行我们自己的程序
        assert(id!=-1);
        if(id==0)
        {
            //因为命令是子进程执行的,真正的重定向工作一定是子进程完成的
            //如何重定向，是父进程要给子进程提供信息，并且这里的重定向不会影响父进程，因为进程具有独立性
            switch(redirType)
            {
                case NONE_REDIR:
                    break;
                case INPUT_REDIR:
                    {
                        int fd = open(redirFile,O_RDONLY);
                        if(fd<0)
                        {
                            perror("open");
                            exit(errno);
                        }
                        //重定向的文件已经成功打开
                        dup2(fd,0);
                    }
                    break;
                case OUTPUT_REDIR:
                case APPEND_REDIR:
                    {
                        umask(0);
                        int flags = O_WRONLY | O_CREAT;
                        if(redirType == APPEND_REDIR) flags |= O_APPEND;
                        else flags |= O_TRUNC;
                        int fd = open(redirFile,flags,0666);
                        if(fd<0)
                        {
                            perror("open fail");
                            exit(errno);

                        }
                        dup2(fd,1);
                    }
                    break;
                default:
                    printf("bug?\n");
                    break;
           }
           
           //将子进程替换成其它程序，如ls
           execvpe(myargv[0],myargv,environ);
           exit(1);
        }
        //shell
        int status=0;
        pid_t ret = waitpid(id,&status,0);//shell等待子进程退出，进行回收
        if(ret>0)
        {
            ;//printf("exit code:%d\n",WEXITSTATUS(status));//打印子进程退出码
        }
        lastCode = ((status>>8) & 0xFF);
        lastSig = (status & 0x7F);
    }
    return 0;
}
